/*
 *  Borum, Boolean rule manipulator.
 *  Copyright (C) 2009 Jon Lennersten, Johan Hedin, Daniel Rosenberg
 *
 *  This file is part of Borum.
 *
 *  Borum is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Borum is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Borum.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mawell.borum;

import net.antonioshome.swing.treewrapper.*;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Enumeration;

public class RuleView extends JPanel {

    private Rule rule = new Rule();

    private Translation trans;
    private RuleModel model;
    private RuleController controller;

    private JScrollPane scroller;
    private JTree tree;
    private OperandNode rootNode;
    private DefaultMutableTreeNode actualTreeNode;

    private JPopupMenu opPopupMenu;
    private JPopupMenu termPopupMenu;

    private JMenu newOpMenu;
    private JMenu newTermMenu;
    private JMenuItem opDeleteItem;

    private JLabel undoLabel;
    private JLabel redoLabel;

    MyCustomPopupHandler customPopupHandler;


    public RuleView(Rule rule, Translation trans, java.util.List<Term> possibleTerms, Object translationForTerms) {
        OperandNode rootNode = rule.getRootNode();
        rootNode.setTrans(trans);

        RuleModel model = new RuleModel();
        model.setPossibleTerms(possibleTerms);

        recursiveInsertNode(rootNode, trans, translationForTerms);
        TreeModel tm = new TreeModel(rootNode);

        model.insertNewModel(tm);

        setRule(rule);
        setName(rule.getName());
        setLocked(rule.isLocked());
        setSelected(rule.isSelected());

        this.controller = new RuleController();
        this.model = model;
        this.trans = trans;
        model.setController(controller);
        model.setView(this);
        controller.setModel(model);
        controller.setView(this);
        this.setLayout(new GridBagLayout());
        customPopupHandler = new MyCustomPopupHandler();
        init();
        TreeWrapper wrapper = new TreeWrapper(tree);
        wrapper.setCustomPopupHandler(customPopupHandler);
        wrapper.addTreeTreeDnDListener(new MyTreeDnDListener());
    }

    public RuleView(RuleController controller, RuleModel model, Translation trans) {
        this.controller = controller;
        this.model = model;
        this.trans = trans;
        model.setController(controller);
        model.setView(this);
        controller.setModel(model);
        controller.setView(this);
        this.setLayout(new GridBagLayout());
        customPopupHandler = new MyCustomPopupHandler();
        init();
        TreeWrapper wrapper = new TreeWrapper(tree);
        wrapper.setCustomPopupHandler(customPopupHandler);
        wrapper.addTreeTreeDnDListener(new MyTreeDnDListener());
    }

    public RuleView(RuleModel model, Translation trans) {
        this(new RuleController(), model, trans);
    }

    public RuleView(RuleView view) {
        this(new RuleModel(view.getModel()), view.getTrans());
        setLocked(false);
        setName(view.getName() + trans.getLabelCopyEnding());
        setEdited(true);
    }

    public RuleModel getModel() {
        return model;
    }

    public void setModel(RuleModel model) {
        this.model = model;
    }

    public Translation getTrans() {
        return trans;
    }

    public void setTrans(Translation trans) {
        this.trans = trans;
    }

    public Rule getRule() {
        TreeModel tm = getModel().getCurrentTreeModel();
        OperandNode rootNode = (OperandNode) tm.getRoot();
        recursively(rootNode);

        rule.setRootNode(rootNode);
        return rule;
    }

    public void recursively(OperandNode aNode) {
        aNode.setOps(new ArrayList<OperandNode>());
        aNode.setTerms(new ArrayList<Term>());
        Enumeration e = aNode.children();

        while(e.hasMoreElements()) {
            Object node = e.nextElement();
            if(node instanceof OperandNode) {
                recursively((OperandNode) node);
                aNode.getOps().add((OperandNode) node);
            } else {
                aNode.getTerms().add((Term) node);
            }
        }
    }

    public void setRule(Rule rule) {
        this.rule = rule;
    }

    public String getName() {
        if(rule == null) {
            return "";
        }
        return rule.getName();
    }

    public void setName(String name) {
        rule.setName(name);
    }

    public boolean isNewRule() {
        return rule.isNewRule();
    }

    public void setNewRule(boolean newRule) {
        rule.setNewRule(newRule);
    }

    public boolean isEdited() {
        return rule.isEdited();
    }

    public void setEdited(boolean edited) {
        rule.setEdited(edited);
    }

    public boolean isLocked() {
        return rule.isLocked();
    }

    public void setLocked(boolean locked) {
        rule.setLocked(locked);
    }

    public boolean isDeleted() {
        return rule.isDeleted();
    }

    public void setDeleted(boolean deleted) {
        rule.setDeleted(deleted);
    }

    public boolean isSelected() {
        return rule.isSelected();
    }

    public void setSelected(boolean selected) {
        rule.setSelected(selected);
    }

    public void expandAll(boolean expand) {
        controller.expandAll(tree, expand);
    }

    private void recursiveInsertNode(OperandNode node, Translation trans, Object translationForTerms) {
        if(node.getOps() != null) {
            for(OperandNode n : node.getOps()) {
                n.setTrans(trans);
                node.insert(n, node.getChildCount());
                recursiveInsertNode(n, trans, translationForTerms);
            }
        }
        if(node.getTerms() != null) {
            for(Term t : node.getTerms()) {
                t.setTranslationForTerm(translationForTerms);
                node.insert(t, node.getChildCount());
            }
        }
    }

    private void init() {
        addButtons();
        addScroller();
    }

    private void addButtons() {
        JPanel buttonPanel = new JPanel();

        java.net.URL url = RuleView.class.getResource("/icons/arrow_undo.png");
        ImageIcon imageIcon = new ImageIcon(url);
        undoLabel = new JLabel(imageIcon);
        undoLabel.setToolTipText(trans.getLabelUndo() + " (Ctrl + Z)");
        undoLabel.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                undo();
            }
        });
        undoLabel.setEnabled(false);
        buttonPanel.add(undoLabel);

        url = RuleView.class.getResource("/icons/arrow_redo.png");
        imageIcon = new ImageIcon(url);
        redoLabel = new JLabel(imageIcon);
        redoLabel.setToolTipText(trans.getLabelRedo() + " (Ctrl + Shift + Z)");
        redoLabel.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                redo();
            }
        });
        redoLabel.setEnabled(false);
        buttonPanel.add(redoLabel);

        JLabel expandLabel;
        url = RuleView.class.getResource("/icons/arrow_out.png");
        imageIcon = new ImageIcon(url);
        expandLabel = new JLabel(imageIcon);
        expandLabel.setToolTipText(trans.getLabelExpand());
        expandLabel.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                controller.expandAll(tree, true);
            }
        });
        buttonPanel.add(expandLabel);

        JLabel collapseLabel;
        url = RuleView.class.getResource("/icons/arrow_in.png");
        imageIcon = new ImageIcon(url);
        collapseLabel = new JLabel(imageIcon);
        collapseLabel.setToolTipText(trans.getLabelCollapse());
        collapseLabel.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                controller.expandAll(tree, false);
            }
        });
        buttonPanel.add(collapseLabel);

        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.NORTHWEST;
        c.weightx = 0; // add horizontal space to this component
        c.weighty = 0; // add vertical space to this component
        c.gridwidth = GridBagConstraints.REMAINDER; // end of row
        c.gridheight = 1;
        c.fill = GridBagConstraints.NONE; // fill vertically and horizontally if bigger than expected
        this.add(buttonPanel, c);
    }

    private void addScroller() {
        opPopupMenu = new JPopupMenu();
        newOpMenu = new JMenu(trans.getLabelNewOperand());
        for(final OperandType type : OperandType.values()) {
            String label;
            switch(type) {
                case AND:
                    label = trans.getLabelForOpAND();
                    break;
                case OR:
                    label = trans.getLabelForOpOR();
                    break;
                default:
                    label = trans.getLabelForOpNOT();
            }
            JMenuItem item = new JMenuItem(label);
            item.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    addOp(type);
                }
            });
            newOpMenu.add(item);
        }
        opPopupMenu.add(newOpMenu);

        newTermMenu = new JMenu(trans.getLabelNewTerm());
        for(final Term term : model.getPossibleTerms()) {
            final JMenuItem item = new JMenuItem(term.getName());
            item.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    addTerm(term);
                }
            });
            newTermMenu.add(item);
        }
        opPopupMenu.add(newTermMenu);

        opDeleteItem = new JMenuItem(trans.getLabelDelete());
        opDeleteItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                if(actualTreeNode.getChildCount() == 0) {
                    model.removeNodeFromParent(actualTreeNode);
                } else
                if(JOptionPane.showConfirmDialog(scroller, trans.getLabelReallyDelete(), trans.getLabelConfirm(), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                    model.removeNodeFromParent(actualTreeNode);
                }
            }
        });
        opPopupMenu.add(opDeleteItem);

        termPopupMenu = new JPopupMenu();
        final JMenuItem changeItem = new JMenuItem(trans.getLabelEdit());
        changeItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                model.doSnapshotOfModel();
                if(!((Term) actualTreeNode).edit(scroller)) {
                    model.revertLastSnapshot();
                }
                updateUndoRedoLabelsAndEditedFlag();
            }
        });
        termPopupMenu.add(changeItem);
        JMenuItem termDeleteItem = new JMenuItem(trans.getLabelDelete());
        termDeleteItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                model.removeNodeFromParent(actualTreeNode);
            }
        });
        termPopupMenu.add(termDeleteItem);

        if(model.getCurrentTreeModel() == null) {
            rootNode = new OperandNode(OperandType.AND);
            rootNode.setTrans(trans);
            model.insertNewModel(new TreeModel(rootNode));

            tree = new JTree(model.getCurrentTreeModel());
        } else {
            tree = new JTree(model.getCurrentTreeModel());
        }

        MouseListener ml = new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                if(e.getClickCount() == 2) {
                    TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
                    Object clickedNode = selPath.getLastPathComponent();
                    if(clickedNode instanceof Term) {
                        model.doSnapshotOfModel();
                        if(!((Term) clickedNode).edit((scroller))) {
                            model.revertLastSnapshot();
                        }
                        updateUndoRedoLabelsAndEditedFlag();
                    }
                }
            }
        };
        tree.addMouseListener(ml);
        tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "ENTER");
        AbstractAction enterAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                Object selectedNode = tree.getSelectionPath().getLastPathComponent();
                if(selectedNode instanceof Term) {
                    model.doSnapshotOfModel();
                    if(!((Term) selectedNode).edit((scroller))) {
                        model.revertLastSnapshot();
                    }
                    updateUndoRedoLabelsAndEditedFlag();
                } else if(selectedNode instanceof OperandNode) {
                    //Show menu
                    OperandNode thisNode = (OperandNode) selectedNode;
                    JPopupMenu menu = customPopupHandler.getMenuAt(tree, thisNode);
                    if(menu != null) {
                        Rectangle r = tree.getRowBounds(tree.getRowForPath(tree.getSelectionPath()));
                        Point p = tree.getLocation();
                        r.x += p.x;
                        r.y += p.y;
                        menu.show(tree, r.x, r.y);
                    }
                }
            }
        };
        tree.getActionMap().put("ENTER", enterAction);
        tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "DELETE");
        AbstractAction deleteAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                if(!isLocked()) {
                    Object selectedNode = tree.getSelectionPath().getLastPathComponent();
                    if(selectedNode instanceof Term) {
                        model.removeNodeFromParent((Term) selectedNode);
                    } else if(selectedNode instanceof OperandNode) {
                        OperandNode thisNode = (OperandNode) selectedNode;
                        if(thisNode.getChildCount() == 0) {
                            model.removeNodeFromParent(thisNode);
                        } else
                        if(JOptionPane.showConfirmDialog(scroller, trans.getLabelReallyDelete(), trans.getLabelDelete(), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
                            model.removeNodeFromParent(thisNode);
                        }
                    }
                }
            }
        };
        tree.getActionMap().put("DELETE", deleteAction);

        tree.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "UNDO");
        AbstractAction undoAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                undo();
            }
        };
        tree.getActionMap().put("UNDO", undoAction);

        tree.getInputMap().put(KeyStroke.getKeyStroke("shift control Z"), "REDO");
        AbstractAction redoAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                redo();
            }
        };
        tree.getActionMap().put("REDO", redoAction);

        tree.setAutoscrolls(true);

        scroller = new JScrollPane();
        scroller.setViewportView(tree);
        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.NORTHWEST;
        c.weightx = 1; // add horizontal space to this component
        c.weighty = 1; // add vertical space to this component
        c.gridwidth = GridBagConstraints.REMAINDER; // end of row
        c.gridheight = 1;
        c.fill = GridBagConstraints.BOTH; // fill vertically and horizontally if bigger than expected

        this.add(scroller, c);
    }

    private void redo() {
        tree.setModel(model.redo());
        controller.expandAll(tree, true);
        updateUndoRedoLabelsAndEditedFlag();
    }

    private void undo() {
        tree.setModel(model.undo());
        controller.expandAll(tree, true);
        updateUndoRedoLabelsAndEditedFlag();
    }

    private void updateUndoRedoLabelsAndEditedFlag() {
        if(model.isModelLast()) {
            redoLabel.setEnabled(false);
        } else {
            redoLabel.setEnabled(true);
        }
        if(model.isModelFirst()) {
            undoLabel.setEnabled(false);
            this.setEdited(false);
        } else {
            undoLabel.setEnabled(true);
            this.setEdited(true);
        }
        model.getCurrentTreeModel().nodeChanged(actualTreeNode);
        fireStateChanged();
        this.getRootPane().repaint();
    }

    private void addOp(OperandType type) {
        OperandNode on = new OperandNode(type);
        on.setTrans(trans);
        model.insertNodeInto(on, actualTreeNode, 0);
        tree.expandPath(new TreePath(actualTreeNode.getPath()));
        updateUndoRedoLabelsAndEditedFlag();
    }

    private void addTerm(Term term) {
        Term copy = (Term) term.deepCopy();

        if(copy.edit(scroller)) {
            model.insertNodeInto(copy, actualTreeNode, 0);
            TreePath path = new TreePath(actualTreeNode.getPath());
            tree.expandPath(path);
            updateUndoRedoLabelsAndEditedFlag();
        }
    }

    /**
     * Adds a <code>ChangeListener</code> to this RuleView.
     *
     * @param l the <code>ChangeListener</code> to add
     * @see #fireStateChanged
     * @see #removeChangeListener
     */
    public void addChangeListener(ChangeListener l) {
        listenerList.add(ChangeListener.class, l);
    }

    /**
     * Removes a <code>ChangeListener</code> from this RuleView.
     *
     * @param l the <code>ChangeListener</code> to remove
     * @see #fireStateChanged
     * @see #addChangeListener
     */
    public void removeChangeListener(ChangeListener l) {
        listenerList.remove(ChangeListener.class, l);
    }

    /**
     * @see #addChangeListener
     * @see javax.swing.event.EventListenerList
     */
    protected void fireStateChanged() {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==ChangeListener.class) {
                ((ChangeListener)listeners[i+1]).stateChanged(new ChangeEvent(this));
            }
        }
    }

    class MyCustomPopupHandler implements CustomPopupHandler {
        public JPopupMenu getMenuAt(JTree aJTree, TreeNode aTreeNode) {

            if(isLocked()) {
                return null;
            }

            actualTreeNode = (DefaultMutableTreeNode) aTreeNode;
            if(aTreeNode instanceof OperandNode) {
                if(((OperandNode) aTreeNode).getType() == OperandType.NOT &&
                        aTreeNode.getChildCount() > 0) {
                    newOpMenu.setEnabled(false);
                    newTermMenu.setEnabled(false);
                } else {
                    newOpMenu.setEnabled(true);
                    newTermMenu.setEnabled(true);
                }
                if(aTreeNode == rootNode) {
                    opDeleteItem.setEnabled(false);
                } else {
                    opDeleteItem.setEnabled(true);
                }
                return opPopupMenu;
            } else {
                return termPopupMenu;
            }
        }

    }

    class MyTreeDnDListener implements TreeTreeDnDListener {
        public void mayDrop(TreeTreeDnDEvent anEvent) throws DnDVetoException {
            if(anEvent.getTargetNode() instanceof OperandNode &&
                    ((OperandNode) anEvent.getTargetNode()).getType() == OperandType.NOT &&
                    anEvent.getTargetNode().getChildCount() > 0) {
                throw new DnDVetoException("NOT nodes can have at most one child");
            }
            if(!(anEvent.getTargetNode() instanceof OperandNode) &&
                    anEvent.getTargetNode().getParent() instanceof OperandNode &&
                    ((OperandNode) anEvent.getTargetNode().getParent()).getType() == OperandType.NOT) {
                throw new DnDVetoException("NOT nodes can have at most one child");
            }

        }

        public void drop(TreeTreeDnDEvent anEvent) throws DnDVetoException {
            model.doSnapshotOfModel();
        }
    }

}
